/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
*/

package com.chasekernan.hxnova.core.stars;

import com.chasekernan.hxnova.core.components.ComponentSet;
import com.chasekernan.hxnova.core.components.Scanner;
import com.chasekernan.hxnova.core.minerals.MineralSet;
import com.chasekernan.hxnova.core.players.Player;
import com.chasekernan.hxnova.utils.Vector;

#if flash9
import flash.display.BitmapData;
import flash.display.Sprite;
#end

/**
    Creates maps based upon some input data.
    Types include rectangle, ring, bezier curve, and bitmap concentrations.
**/
class MapCreator {
    
    public static var DEFAULT_FACTORIES : Int = 10;
    public static var DEFAULT_MINES : Int = 10;
    public static var DEFAULT_DEFENSES : Int = 10;
    public static var DEFAULT_POP : Int = 50000;
    public static var DEFAULT_STARTING_MIN : Int = 2000;
    
    private static var NULL_PLAYER = new Player();
    
    private static function createBasicStars(starPoints : Array < Vector > , 
            names : Array < String > , stars : Array < Star > ) {
        NULL_PLAYER.id = -1;
        
        for (i in 0...starPoints.length) {
            var s : Star = new Star();
            s.location = starPoints[i];
            s.name = names[i];
            s.minerals = new MineralSet();
            s.concentrations = new Concentrations(false);
            s.environment = new Environment();
            s.defenses = s.mines = s.factories = 0;
            s.hasScanner = false;
            s.owner = NULL_PLAYER;
            s.population = 0;
            stars.push(s);
        }
    }
    
    /**
        Creates a rectangular map and returns the array of stars (which contains
        both hws and regular systems).
    **/
    public static function generateRectMap(dimensions : Vector, 
            numberOfStars : Int, players : Array<Player>, names : Array<String>,
            ?hwMinConc : Concentrations) : Array<Star> {
        
        var stars : Array<Star> = new Array<Star>();
        hwMinConc = if(hwMinConc == null) new Concentrations(true) 
                else hwMinConc;
        
        players = cast(randomizeArray(players.copy()));
        names = cast(randomizeArray(names));
        
        var starPoints : Array<Vector> = createRectPoints(dimensions, 
                numberOfStars, 0.05);
        
        createBasicStars(starPoints, names, stars);
        
        var hwPoints : Array<Vector> = createRectPoints(dimensions, 
                players.length, 0.3);
        
        for(i in 0...hwPoints.length) {
            //hw loc
            var point : Vector = hwPoints[i];
                
            //find closest star
            var dist : Float = 999999;
            var curDist : Float = 0;
            var starNum : Int = 0;
                
            for (j in 0...stars.length) {
                if (stars[j].isHomeworld != null && stars[j].isHomeworld) 
                    continue;
                    
                curDist = stars[j].location.distanceTo(point);
                if(curDist < dist) {
                    dist = curDist;
                    starNum = j;
                }
            }
                
            //found the star so now change it to HW
            var star : Star    = stars[starNum];
            var player : Player    = players[i];
            
            convertStar(star, player, hwMinConc);
        }

        return stars;
    }
    
    //converts a star into a homeworld
    private static function convertStar(star : Star, player : Player, 
            conc : Concentrations) {
        star.owner = player;
        star.population = DEFAULT_POP;
        player.homeworld = star;
        star.isHomeworld = true;
        star.factories = DEFAULT_FACTORIES;
        star.mines = DEFAULT_MINES;
        star.defenses = DEFAULT_DEFENSES;
        star.concentrations = conc.clone();
        star.minerals = new MineralSet();
        star.production = new ProductionQueue(star);
        star.hasScanner = true;
        
        for (i in Concentrations.MINERAL_TYPES) 
            star.minerals.setValue(i, Std.int(DEFAULT_STARTING_MIN * 
                                              conc.getValue(i) / 100.0));
            
        star.environment = player.race.habSetting.getPerfectEnv();
    }
    
    /**
        Creates a ring filled with stars and returns the array of stars with 
        both hws and regular systems.
    **/
    public static function generateRingMap(outerRadius : Float, 
            innerRadius : Float, numberOfStars : Int, players : Array<Player>, 
            names : Array<String>, ?hwMinConc : Concentrations) : Array<Star> {
        
        var stars : Array<Star> = new Array<Star>();
        hwMinConc = if(hwMinConc == null) new Concentrations() else hwMinConc;
        
        players = cast(randomizeArray(players));
        names = cast(randomizeArray(names));
        
        var starPoints : Array<Vector> = 
                createRingPoints(outerRadius, innerRadius, numberOfStars);
        
        createBasicStars(starPoints, names, stars);
        
        var hwPoints : Array<Vector> = 
                createRingPoints(outerRadius, innerRadius, players.length);
        
        for(i in 0...hwPoints.length) {
            //hw loc
            var point : Vector = hwPoints[i];
                
            //find closest star
            var dist : Float = 999999;
            var curDist : Float = 0;
            var starNum : Int = 0;
                
            for(j in 0...stars.length) {
                curDist = stars[j].location.distanceTo(point);
                if(curDist < dist) {
                    dist = curDist;
                    starNum = j;
                }
            }
                
            //found the star so now change it to HW
            var star : Star    = stars[starNum];
            var player : Player    = players[i];
            
            convertStar(star, player, hwMinConc);
        }
        
        return stars;
    }
    
    private static function randomizeArray(a : Array<Dynamic>) :
            Array<Dynamic> {
        a = a.copy();
        var newA : Array<Dynamic> = new Array<Dynamic>();
        var i : Int = a.length - 1;
        while(i >= 0) {
            newA.push(a.splice(Math.floor(
                    Math.random() * (a.length) - 0.000001), 1)[0]);
            i --;
        }
        return newA;
    }
    
    private static function createRectPoints(dimensions : Vector, 
            numberOfPoints : Int, borderPercent : Float):Array<Vector> {
        var points : Array<Vector> = new Array<Vector>();
        var sqrt : Int = Math.ceil(Math.sqrt(numberOfPoints));
        var plotSize : Vector = 
                new Vector(dimensions.x / sqrt, dimensions.y / sqrt);
        var excessPlots :Int = Math.floor(sqrt * sqrt - numberOfPoints);
        var border : Float = borderPercent * Math.min(plotSize.x, plotSize.y);
            
        //remove excess
        var randomIntX : Int;
        var randomIntY : Int;
        var remove : Array<Array<Bool>> = new Array<Array<Bool>>();
        
        //setup array
        for(i in 0...sqrt) {
            remove[i] =    new Array<Bool>();
            for(j in 0...sqrt) remove[i][j] = true;
        }
            
        //remove extra
        for(i in 0...excessPlots) {
            do {
                randomIntX = Math.floor(Math.random() * (sqrt - 0.0001));
                randomIntY = Math.floor(Math.random() * (sqrt - 0.0001));
            } while (remove[randomIntX][randomIntY] == false);
            remove[randomIntX][randomIntY] = false;
        }
            
        //setup actual points
        for(i in 0...sqrt) {
            for(j in 0...sqrt) {
                if(remove[i][j] == false) continue;
                
                var xloc : Int = Math.floor(i * plotSize.x + border + 
                        Math.random() * (plotSize.x - border * 2));
                var yloc : Int = Math.floor(j * plotSize.y + border + 
                        Math.random() * (plotSize.y - border * 2));
                    
                points.push(new Vector(xloc, yloc));
            }
        }
        return points;
    }
    
    private static function createRingPoints(outerRadius : Float, 
            innerRadius : Float, numberOfPoints : Int) : Array<Vector> {
        //divide randomly into angles 
        var angleWidth : Float = Math.PI * 2 / numberOfPoints;
        var angles : Array<Float> = new Array<Float>();
        
        for(i in 0...numberOfPoints) {
            angles.push((i * angleWidth) + Math.random() * (0.9 * angleWidth) +
                    0.1 * angleWidth);
        }
        
        //now place the points
        var points : Array<Vector> = new Array<Vector>();
        for(i in 0...numberOfPoints) {
            var length : Float = Math.random() * (outerRadius - innerRadius) + 
                    innerRadius;
            var vec : Vector = new Vector(outerRadius, outerRadius);
            vec.x += length * Math.cos(angles[i]);
            vec.y += length * Math.sin(angles[i]);
            points.push(vec);
        }
        
        return points;
    }
    
    /**
        Creates a map based off of a cubic bezier equation. Essentially, it 
        expands the curve by the amount radius, so that it forms a solid and 
        fills that with stars.
    **/
    public static function generateCubicBezierMap(eq : CubicBezier, 
            radius : Float, numberOfStars : Int, players : Array<Player>, 
            names : Array<String>, ?hwMinConc : Concentrations) : Array<Star> {
        var stars = new Array<Star>();
        
        hwMinConc = if(hwMinConc == null) new Concentrations() else hwMinConc;
        
        players = cast(randomizeArray(players));
        names = cast(randomizeArray(names));
        
        var starPoints : Array<Vector> = 
                createBezierPoints(eq, radius, numberOfStars, 100);
        
        createBasicStars(starPoints, names, stars);
        
        var hwPoints : Array<Vector> = 
                createBezierPoints(eq, radius, players.length, 10);
        
        for(i in 0...hwPoints.length) {
            //hw loc
            var point : Vector = hwPoints[i];
                
            //find closest star
            var dist : Float = 999999;
            var curDist : Float = 0;
            var starNum : Int = 0;
                
            for(j in 0...stars.length) {
                curDist = stars[j].location.distanceTo(point);
                if(curDist < dist) {
                    dist = curDist;
                    starNum = j;
                }
            }
                
            //found the star so now change it to HW
            var star : Star    = stars[starNum];
            var player : Player    = players[i];
            
            convertStar(star, player, hwMinConc);
        }
        
        return stars;
    }
    
    private static function createBezierPoints(eq : CubicBezier, radius : Float,
            numberOfPoints : Int, divisions : Int) : Array<Vector> {
        var points = new Array<Vector>();
        
        var lines = eq.getLines(divisions);
        var totalLength : Float = 0;
        for(i in lines) {
            totalLength += i.length;
        }
        var lengthPerPoint = totalLength / numberOfPoints;
        
        var line : Line;
        var cLine : Int = 0;
        var cLength : Float = 0;
        for(i in 0...numberOfPoints){
            line = lines[cLine];
            points.push(line.getNormalPoint(radius, cLength));
            
            cLength += lengthPerPoint;
            if(cLength > line.length) {
                cLength -= line.length;
                cLine ++;
            }
        }
        
        
        return points;
    }
    
    private static function genHWs(stars : Array<Star>, points : Array<Vector>, 
            players : Array<Player>, hwMinConc : Concentrations) : Void {
        for(i in 0...points.length) {
            //hw loc
            var point : Vector = points[i];
                
            //find closest star
            var dist : Float = 999999;
            var curDist : Float = 0;
            var starNum : Int = 0;
                
            for(j in 0...stars.length) {
                curDist = stars[j].location.distanceTo(point);
                if(curDist < dist) {
                    dist = curDist;
                    starNum = j;
                }
            }
                
            //found the star so now change it to HW
            var star : Star    = stars[starNum];
            var player : Player    = players[i];
            
            convertStar(star, player, hwMinConc);
        }
    }
    
    #if flash9
    /**
        Generates the map from a bitmap, where the white pixels represent 
        possible locations for stars.
        Currently only works on flash9 (BitmapData class) but can be ported to
        neko/js (maybe php?).
    **/
    public static function createMapFromBitmap(bitmap : BitmapData, 
            numberOfStars : Int, players : Array<Player>, names : Array<String>,
            ?hwMinConc : Concentrations) : Array<Star> {
        var stars = new Array<Star>();
        
        hwMinConc = if(hwMinConc == null) new Concentrations() else hwMinConc;
        
        players = cast(randomizeArray(players));
        names = cast(randomizeArray(names));
        
        var starPoints : Array<Vector> = 
                createBitmapPoints(bitmap.clone(), numberOfStars);
        
        createBasicStars(starPoints, names, stars);
        createBasicStars(starPoints, names, stars);
        
        genHWs(stars, createBitmapPoints(bitmap.clone(), players.length), 
                players, hwMinConc);
        
        return stars;
    }
    
    private static function createBitmapPoints(bitmap : BitmapData, 
            numberOfPoints : Int, ?minDist : Float, ?retries : Int) : 
            Array<Vector> {
        if(retries == null) retries = 2000;
        
        
        var points = new Array<Vector>();
        
        //calculate number of white points
        var wPoints = calcWhitePoints(bitmap);
        if(minDist == null) minDist = Math.pow(wPoints.length, 9 / 10) * 0.03 / 
                numberOfPoints;
        
        for(i in 0...numberOfPoints) {
            var j : Int = -1;
            var placed = false;
            do {
                j++;
                var p = wPoints[Std.random(wPoints.length)];
                var flag = false;
                for(k in points) if(p.distanceTo(k) < minDist) {
                    flag = true;
                    break;
                }
                if(flag) continue;
                points.push(p);
                placed = true;
                break;
            } while(j < retries);
            if(placed = false) points.push(wPoints[Std.random(wPoints.length)]);
        }
        
        return points;
    }
    
    private static function calcWhitePoints(bitmap : BitmapData) : 
            Array<Vector> {
        var points = new Array<Vector>();
        for(x in 0...bitmap.width) {
            for(y in 0...bitmap.height){ 
                if(bitmap.getPixel(x, y) == 0xFFFFFF) 
                    points.push(new Vector(x, y));
            }
        };
        return points;
    }
    #end
}

/**
    Class used to create a bezier line.
**/
class CubicBezier{
    public var p0 : Vector;
    public var p1 : Vector;
    public var p2 : Vector;
    public var p3 : Vector;
    
    public var ax : Float;
    public var bx : Float;
    public var cx : Float;
    
    public var ay : Float;
    public var by : Float;
    public var cy : Float;
    
    public function new(p0 : Vector, p1 : Vector, p2 : Vector, p3 : Vector) {
        this.p0 = p0;
        this.p1 = p1;
        this.p2 = p2;
        this.p3 = p3;
        
        cx = 3 * (p1.x - p0.x);
        bx = 3 * (p2.x - p1.x) - cx;
        ax = p3.x - p0.x - cx - bx;

        cy = 3 * (p1.y - p0.y);
        by = 3 * (p2.y - p1.y) - cy;
        ay = p3.y - p0.y - cy - by;
    }
    
    public function getPoint(t : Float) : Vector {
        //t is between 0 and 1
        var vec = new Vector();
        
        vec.x = ax * t * t * t + bx * t * t + cx * t + p0.x;
        vec.y = ay * t * t * t + by * t * t + cy * t + p0.y;
        
        return vec;
    }
    
    public function getLines(divisions : Int) : Array<Line> {
        var a : Array<Line> = new Array<Line>();
        var seg : Float = 1 / divisions;
        for(i in 0...divisions) {
            a.push(new Line(getPoint(i * seg), getPoint(i * seg + seg)));
        }
        
        return a;
    }
}

//THIS IS A STATIC LINE DO NOT TRY TO CHANGE THE ENDPOINTS (IT WONT LET YOU)

/**
    Line used by the bezier engine to approximate the bezier segments.
**/
class Line {
    public var p0(default, null) : Vector;
    public var p1(default, null) : Vector;
    
    public var length(default, null) : Float;
    //public var slope(default, null) : Float;
    
    public function new(p0 : Vector, p1 : Vector) {
        this.p0 = p0;
        this.p1 = p1;
        length = Math.sqrt((p0.x - p1.x) * (p0.x - p1.x) + (p0.y - p1.y) * 
                (p0.y - p1.y));
        //slope = (p1.y - p0.y) / (p1.x - p0.x);
    }
    
    public function getPointAtPercent(p : Float) : Vector{
        var angle = p1.getSub(p0).getAngle();
        var clength = p * length;
        return new Vector(clength * Math.cos(angle), clength  * 
                Math.sin(angle)).getAdd(p0);
    }
    
    public function getPointByLength(l : Float) : Vector {
        return getPointAtPercent(l / length);
    }
    
    //l is how far along the line the normal is
    public function getNormalPoint(radius : Float, l : Float) : Vector {
        var p = getPointByLength(l);
        var angle = p1.getSub(p0).getAngle() + 
                if(Math.random() < 0.5) Math.PI / 2 else - Math.PI / 2;
        var nl = Math.random() * radius;
        return new Vector(nl * Math.cos(angle), nl * Math.sin(angle)).getAdd(p);
    }
}